//  Author:
//       Daniel Großer <olav.daniel.grosser@googlemail.com>
//  
//  Copyright (c) 2012 Copyright (C) 2012 by Daniel Großer. This Code is under GNU LGPL 3.0
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Lesser General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Lesser General Public License for more details.
// 
//  You should have received a copy of the GNU Lesser General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Beauty.Contract;

namespace Beauty.Tools
{
    /// <summary>
    /// The scanner is a part of software that takes the string (program source) as input
    /// and offers ways to peek terminals from the input. This is the built-in default scanner
    /// that should suffice for common purposes. It works on a per-line basis to increase
    /// speed and reduce memory usage.
    /// </summary>
    public sealed class Scanner : IDisposable, IScanner
    {
        #region Properties
        /// <summary>
        /// The input reader.
        /// </summary>
        private readonly TextReader Input;

        /// <summary>
        /// Get the current line number.
        /// </summary>
        public uint Line { get; private set; }

        /// <summary>
        /// Get the current column.
        /// </summary>
        public uint Column { get; private set; }

        /// <summary>
        /// Get, whether the end of the input has been reached.
        /// </summary>
        public bool EOF { get; private set; }

        private StringBuilder m_RestOfLine;
        /// <summary>
        /// Ge the rest of current source line that has not been consumed yet.
        /// </summary>
        private StringBuilder RestOfLine
        {
            get
            {
                if (EOF) throw new EndOfStreamException();

                if (m_RestOfLine == null || m_RestOfLine.Length == 0)
                {
                    var nextLine = Input.ReadLine();
                    m_RestOfLine = nextLine != null ? new StringBuilder(nextLine) : null;
                    Line++;
                    Column = 1;
                    EOF = m_RestOfLine == null;
                }

                return m_RestOfLine;
            }
        }

        private Func<char, bool> m_CanSkip = Char.IsWhiteSpace;
        /// <summary>
        /// Get/Set the logic that decides which characters of the input can be ommitted.
        /// By default, this is <see cref="CanSkipWhitespaces"/>, so whitespace characters
        /// are not processed as part of the language.
        /// </summary>
        public Func<char, bool> CanSkip
        {
            get
            {
                return m_CanSkip;
            }
            set
            {
                m_CanSkip = value;
            }
        }
        #endregion

        #region Dispose pattern
        /// <summary>
        /// Dispose
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        /// <summary>
        /// Dispose or finalize.
        /// </summary>
        /// <param name="disposing">true iff disposing.</param>
        private void Dispose(bool disposing)
        {
            if (disposing)
                if (Input != null)
                {
                    Input.Dispose();
                }
        }
        /// <summary>
        /// Finalize.
        /// </summary>
        ~Scanner()
        {
            Dispose(false);
        }
        #endregion

        /// <summary>
        /// Create a new instance.
        /// </summary>
        /// <param name="input">The input to process.</param>
        public Scanner(TextReader input)
        {
            Input = input;
        }

        /// <summary>
        /// Advance. That is, consume any skippable characters.
        /// </summary>
        private void Advance()
        {
            while (RestOfLine != null)
            {
                if (CanSkip != null && CanSkip(RestOfLine[0]))
                {
                    Advance(1);
                }
                else
                {
                    break;
                }
            }
        }

        /// <summary>
        /// Advance a specifified amount of characters.
        /// </summary>
        /// <param name="count">The count of characters to advance.</param>
        private void Advance(int count)
        {
            RestOfLine.Remove(0, count);
            Column += Convert.ToUInt32(count);
        }
		
		/// <summary>
		/// Setup the scanner.
		/// </summary>
		/// <param name='terminals'>
		/// The set of all known terminals.
		/// </param>
		public void Setup(ICollection<TerminalAttribute> terminals)
		{
			Terminals = new Dictionary<byte, List<TerminalAttribute>>();
			foreach (var terminal in terminals)
			{
				if (!Terminals.ContainsKey(terminal.ScannerPrecedence))
					Terminals.Add(terminal.ScannerPrecedence, new List<TerminalAttribute>());
				Terminals[terminal.ScannerPrecedence].Add(terminal);
			}
		}	
		
		/// <summary>
		/// Gets the set of terminals recognized by this scanner, grouped by precedence.
		/// </summary>
		/// <value>
		/// The set of recognized terminals.
		/// </value>
		private Dictionary<byte, List<TerminalAttribute>> Terminals { get; set; }
		
		/// <summary>
		/// Gets the peek terminal from the input. This automatically creates peeks
		/// for anything but regex stuff - RegEx must be peeked on the fly.
		/// </summary>
		/// <value>
		/// The peek terminal. Null on syntax error.
		/// </value>
		private MatchResult Peek
		{
			get
			{
				if (_Peek == null)
				{
					if (!EOF)
						Advance(); // Skip skippable data
					
					
					if (!EOF)
					{
						for (byte precedence = byte.MinValue; _Peek == null && precedence < byte.MaxValue; precedence++)
						{
							if (precedence == RegExTerminalAttribute.PRECEDENCE ||
								!Terminals.ContainsKey(precedence)) continue;
							// Get best match of this precedence level
							_Peek = BestMatch(Terminals[precedence]);
						}							
					}
				}
				return _Peek;
			}
		}
		private MatchResult _Peek;
		
		/// <summary>
		/// Gets the current token.
		/// </summary>
		/// <value>
		/// The current token.
		/// </value>
		public string CurrentToken
		{
			get
			{
				if (Peek != null)
					return Peek.MatchingTerminal.ToString();
				else
				{
					if (EOF)
						return "End Of Line";
					else
						return RestOfLine.ToString();
				}
			}
		}

        /// <summary>
        /// Consume the current token.
        /// </summary>
        /// <param name="match">The match to consume.</param>
        /// <returns><see cref="Peek"/></returns>
        public MatchResult Consume()
        {
			if (Peek == null)
				throw new InvalidOperationException("Cannot advance after mismatch.");
			var returnValue = Peek;
            Advance(Peek.MatchedLength);
			_Peek = null; // <-- next call to Peek will auto-recognize keywords and delimeters
			// and next call to CanConsume will recognize regular expressions.
			if (!EOF)
				Advance();
			return returnValue;
        }
		
		/// <summary>
		/// Determines whether this instance can consume any of the specified terminals.
		/// </summary>
		/// <returns>
		/// <c>true</c> if this instance can consume any of the specified terminals; otherwise, <c>false</c>.
		/// </returns>
		/// <param name='terminals'>
		/// The expected terminals.
		/// </param>
		public bool CanConsume(IEnumerable<TerminalAttribute> terminals)
		{
			if (Peek == null)
				_Peek = BestMatch(terminals); // this would help to peek regular expressions ONCE and as LATE as possible
			
			if (Peek != null)
			{
				return terminals.Contains(Peek.MatchingTerminal);
			}
			else
			{			
				return false;
			}
		}

        /// <summary>
        /// Determine the best match from a set of possible/expected terminals.
        /// </summary>
        /// <param name="terminals">A set of possible/expected terminals.</param>
        /// <returns>The best match.</returns>
        private MatchResult BestMatch(IEnumerable<TerminalAttribute> terminals)
        {		
			
			if (!EOF)
			{
				TerminalAttribute bestMatchingTerminal = null;
				int bestMatchedLength = 0;
	            string restOfLine = RestOfLine.ToString();
	
	            foreach (var terminal in terminals)
	            {
	                var candidateMatchLength = terminal.MatchStart(restOfLine);
	                if (candidateMatchLength > bestMatchedLength)
	                {
	                    bestMatchingTerminal = terminal;
	                    bestMatchedLength = candidateMatchLength;
	                }
	            }
				
				if (bestMatchingTerminal != null)
					return new MatchResult(bestMatchingTerminal, restOfLine.Substring(0, bestMatchedLength));
				else
					return null;
			}
			else
				return null;			
            
        }
    }
}
